Chapter 8: Handling Errors

Error (or exception) handling is an essential feature of writing all but trivial programs.

Let's face it – s**t happens, and sometimes the best-written programs encounter errors. Well-written code handles errors gracefully and as early as possible.

Over the years, two main 'approaches' to error handling have emerged:

  • The LBYL approach (Look before you leap) support validating every of data well before they are used and only use data that has passed the test. LBYL code is lengthy, and looks very solid.
  • More recently, the EAFP has emerged, asserting the old Marine Corps motto that it is easier to ask forgiveness than permission. EAFP code relies heavily on exception handling and try/catch constructs to deal with the occasional consequences of having leapt before looking.

While EAFP is generally regarded with more favour in recent years than LBYL, especially in the Python community, which all but adopted it as its official mantra, both approaches have merits (and serious drawbacks). Julia is particularly suited to an amalgam of the two methods, so whichever of them suits you, your coding style and your use case more, you will find Julia remarkably accommodating.

Creating and Raising exceptions

Julia has a number of built-in exception types, each of which can be thrown when unexpected conditions occur:

<TODO: Table of exceptions and their meanings>

Exception Definition
ArgumentError ...
BoundsError ...
CompositeException ...
DivideError ...
DomainError ...
EOFError ...
ErrorException ...
InexactError ...
InitError ...
InterruptException ...
InvalidStateException ...
KeyError ...
LoadError ...
OutOfMemoryError ...
ReadOnlyMemoryError ...
RemoteException ...
MethodError ...
OverflowError ...
ParseError ...
SystemError ...
TypeError ...
UndefRefError ...
UndefVarError ...
UnicodeError ...

Note that these are exception types, rather than particular exceptions, therefore despite their un-function-like appearance, they will need to be called, using parentheses.

Throwing Exceptions

The throw function allows you to raise an exception:


In [3]:
function checkCircumference(circumference)
    if circumference > 0
        circumference/2π
    elseif circumference == 0
        throw(DivideError())
    else
        throw(DomainError())
    end
end


Out[3]:
checkCircumference (generic function with 1 method)

In [4]:
checkCircumference(0) # Divide Error


LoadError: DivideError: integer division error
while loading In[4], in expression starting on line 1

 in checkCircumference at In[3]:5

In [5]:
checkCircumference(-1) # Domain Error


LoadError: DomainError:
while loading In[5], in expression starting on line 1

 in checkCircumference at In[3]:7

As noted above, exception types need to be called via parnethesis to get an Exception object. Hence, throw(DomainError) would be incorrect.

In addition, some exceptions take arguments that elucidate upon the error at hand. For instance, UndefVarError takes a symbol as an argument, referring to the symbol invoked without being defined:


In [6]:
throw(UndefVarError(thisvariabledoesnotexist))


LoadError: UndefVarError: thisvariabledoesnotexist not defined
while loading In[6], in expression starting on line 1

Throwing a Generic ErrorException

The error function throws a generic ErrorException.

This will interrupt execution of the function or block immediately. Consider the following example, courtesy of Julia's official documentation.

For example, let us define a function fussy_sqrt that raises an ErrorException using the function error if x < 0:


In [7]:
fussy_sqrt(x) = x >= 0 ? sqrt(x) : error("negative x not allowed")


Out[7]:
fussy_sqrt (generic function with 1 method)

Then, the following verbose wrapper is created:


In [11]:
"""
A more verbose square root that wraps up the inline function and prints information around it.
"""
function verbose_fussy_sqrt(x)
	         println("before fussy_sqrt")
	         r = fussy_sqrt(x)
	         println("after fussy_sqrt")
	         return r
	       end


Out[11]:
verbose_fussy_sqrt (generic function with 1 method)

Now, if fussy_sqrt encounters an argument x < 0, an error is raised and execution is aborted. In that case, the second message (after fussy_sqrt) would never come to be displayed:


In [12]:
verbose_fussy_sqrt(2)


before fussy_sqrt
after fussy_sqrt
Out[12]:
1.4142135623730951

In [14]:
verbose_fussy_sqrt(-1) # Throws the error inside the interior function fussy_sqrt()


before fussy_sqrt
LoadError: negative x not allowed
while loading In[14], in expression starting on line 1

 in verbose_fussy_sqrt at In[11]:6

Creating your own exceptions

You can create your own custom exception that inherits from the superclass Exception by


In [24]:
type MyException <: Exception # Inherits class using <: 
end

If you wish your exception to take arguments, which can be useful in returning a useful error message, you will need to amend the above data type to include fields for the the arguments, and create a method under Base.showerror that implements the new error message:


In [25]:
type MyExceptionTree <: Exception # Inherits Exception class
    var::AbstractString           # Take variable of type AbstractString
end

In [26]:
Base.showerror(io::IO, e::MyExceptionTree) = print(io, "Something is wrong with ", e.var, "!")


Out[26]:
showerror (generic function with 27 methods)

In [28]:
throw(MyExceptionTree("this code"))


LoadError: Something is wrong with this code!
while loading In[28], in expression starting on line 1

Handling exceptions

The try/catch structure

Using the keywords try and catch, you can handle exceptions, both generally and dependent on a variable. The general structure of try/catch is as follows:

  1. try block: This is where you would normally introduce the main body of your function. Julia will attempt to execute the code within this section.
  2. catch: The catch keyword, on its own, catches all errors. It is helpful to instead use it with a variable, to which the exception will be assigned, e.g. catch err.
  3. If the exception was assigned to a variable, testing for the exception: using if/elseif/else structures, you can test for the exception and provide ways to handle it. Usually, type assertions for errors will use isa(err, ErrorType), which will return true if err is an instance of the error type ErrorType (i.e. if it has been called by ErrorType()).
  4. end all blocks.

This structure is demonstrated by the following function, creating a resilient, non-fussy sqrt() implementation that returns the complex square root of negative inputs using the catch syntax:


In [29]:
"""
Function that takes a Number type, attempts to square root it, and attempts a complex square root if there is a Domain Error. 
"""

function resilient_square_root(x::Number)
    try
        sqrt(x)
    catch err
        if isa(err, DomainError)
            sqrt(complex(x))
        end
    end
	end


Out[29]:
resilient_square_root (generic function with 1 method)

There is no need to specify a variable to hold the error instance.

Similarly to not testing for the identity of the error, such a clause would result in a catch-all sequence.

This is not necessarily a bad thing, but good code is responsive to the nature of errors, rather than their mere existence, and good programmers would always be interested in why their code doesn't work, not merely in the fact that it failed to execute. Therefore, good code would check for the types of exceptions and only use catch-alls sparingly.

One-line try/catch

If you are an aficionado of brevity, you should be careful when trying to put a try/catch expression. Consider the following code:


In [30]:
try sqrt(x) catch y end

To Julia, this means try sqrt(x), and if an exception is raised, pass it onto the variable y, when what you probably meant is return y. For that, you would need to separate y from the catch keyword using a semicolon:


In [31]:
try sqrt(x) catch; y end # UndefVarError


LoadError: UndefVarError: y not defined
while loading In[31], in expression starting on line 1

In [39]:
x = -3
y = "This won't work!"

try sqrt(x) catch; y end # If try fails, return the y variable


Out[39]:
"This won't work!"

finally clauses

Once the try/catch loops have finished, Julia allows you to execute code that has to be executed whether the operation has succeeded or not; finally executes whether there was an exception or not.

This is important for 'teardown' tasks, gracefully closing files and dealing with other stateful elements and resources that need to be closed whether there was an exception or not.

Consider the following example from the Julia documentation, which involves opening a file, something we have not dealt with yet explicitly. open("file") opens a file in path file, and assigns it to an object, f. It then tries to operate on f.

Whether those operations are successful or not, the file will need to be closed. finally allows for the execution of close(f), closing down the file, regardless of whether an exception was raised in the code in the try section:


In [40]:
# Psuedo-code to illustrate the idea
f = open("file")
try
    # operate on file f
finally
    close(f)
end


LoadError: SystemError: opening file file: No such file or directory
while loading In[40], in expression starting on line 1

 in open at /Applications/Julia-0.4.6.app/Contents/Resources/julia/lib/julia/sys.dylib
 in open at iostream.jl:99

In [41]:
# Application using the square root example
# The function will fail due to a DomainError (negative number in sqrt() but finally will always run)


x = -3

try
    sqrt(x)
finally
    println("Print this in the end, whether or not this function works!")
end


Print this in the end, whether or not this function works!
LoadError: DomainError:
while loading In[41], in expression starting on line 5

It's good practice to ensure that teardown operations are executed regardless of whether the actual main operation has been successful, and finally is a great way to achieve this end.

Advanced error handling

info and warn

We have seen that calling error will interrupt execution.

What, however, if we just want to display a warning or an informational message without interrupting execution, as is common in debugging code? Julia provides the info and warn functions, which allow for the display of notifications without raising an interrupt:


In [42]:
info("This code is looking pretty good.")


INFO: This code is looking pretty good.

In [43]:
warn("You're not looking too good. Best check yourself.")


WARNING: You're not looking too good. Best check yourself.

In [47]:
# You can even use it inline or grab local variables
# Much nicer than random print or println statements

info("I'm square rooting", sqrt(27))
y = sqrt(27)^ 2
info("Now I'm squaring it back to get ", y, "!")


INFO: I'm square rooting5.196152422706632
INFO: Now I'm squaring it back to get 27.0!

In [55]:
# Warnings work exactly the same way


warn("I'm cube rooting ",27," to get ",(27)^(1/3))
z = ((27)^(1/3))^3
info("Now I'm cubing it back to get ", z, "!")


WARNING: I'm cube rooting 27 to get 3.0
INFO: Now I'm cubing it back to get 27.0!

rethrow, backtrace and catch_backtrace

Julia provides three functions that allow you to delve deeper into the errors raised by an operation.

  • rethrow, as the name suggests, raises the last raised error again,
  • backtrace executes a stack trace at the current point, and
  • catch_backtrace gives you a stack trace of the last caught error.

Consider our resilient square root function from the listing above. Using rethrow(), we can see exceptions that have been handled by the function itself:


In [62]:
# Negative, so its converted to a complex number and then rooted
resilient_square_root(-2.345)


Out[62]:
0.0 + 1.5313392831113555im

In [63]:
# Must be used in the same block as the function
# Tosses back the last error in a function, whether or not it was handled

resilient_square_root(-2.345)
rethrow()


LoadError: DomainError:
while loading In[63], in expression starting on line 5

 in resilient_square_root at In[29]:7

As it's evident from this example, rethrow() does not require the error to be actually one that is thrown - if the error itself is handled, it will still be retrieved by rethrow().

backtrace return stack traces at the time of call and catch_backtrace returns the last caught exception:


In [66]:
resilient_square_root(-4)


Out[66]:
0.0 + 2.0im

In [67]:
x^2 - 2x + 3


Out[67]:
18

In [68]:
backtrace()


Out[68]:
18-element Array{Ptr{Void},1}:
 Ptr{Void} @0x000000010eec8e0d
 Ptr{Void} @0x000000031cface90
 Ptr{Void} @0x000000031cface10
 Ptr{Void} @0x000000010ee5ecf6
 Ptr{Void} @0x000000010eec36a4
 Ptr{Void} @0x000000010eec1b3f
 Ptr{Void} @0x000000010eed5e89
 Ptr{Void} @0x000000010eed643d
 Ptr{Void} @0x000000010ee6227c
 Ptr{Void} @0x000000031366ca6c
 Ptr{Void} @0x000000010ee5ec83
 Ptr{Void} @0x000000031364d8f3
 Ptr{Void} @0x000000031364cf97
 Ptr{Void} @0x000000010ee5ec83
 Ptr{Void} @0x000000031363c885
 Ptr{Void} @0x000000010ee5ec83
 Ptr{Void} @0x000000031363f098
 Ptr{Void} @0x000000010eec9ca8

In [69]:
catch_backtrace()


Out[69]:
0-element Array{Ptr{Void},1}

The first backtrace block shows the stack trace for the time after the function x^2 - 2x + 3 has been executed.

The second stacktrace, invoked by the catch_backtrace() call, shows the call stack as it was at the time of the catch in the resilient_square_root function (which converted it into a complex number due to a

Julia is what is described as a functional programming language, meaning that functions are the principal building blocks of a Julia program (as opposed to objects and their instances in OOP).

Introducing functions is the last part we are missing before we can start building fully-fledged applications to solve real world problems. Let's get cracking!

Syntax and Arguments

General syntax and invocation

There are two general ways to define a function. The first way is usually suited for simple, single-expression functions, while the second way is more suitable for longer functions that include multiple expressions.

Single expression functions

Single expression functions are written very similarly to their mathematical form:


In [1]:
geom_average(a,b) = sqrt(a^2 + b^2)


Out[1]:
geom_average (generic function with 1 method)

In [2]:
geom_average(3,4)


Out[2]:
5.0

Multiple expression functions

If your function is more complex, and needs to evaluate multiple functions, this syntax is no longer suitable.

The syntax to use in such cases uses a block, introduced by function and terminated by end, to describe the function:


In [3]:
function breakfast(pancakes, coffee)
           println("$coffee cups of coffee and $pancakes pancakes, please.")
       end


Out[3]:
breakfast (generic function with 1 method)

In [4]:
breakfast(2,4)


4 cups of coffee and 2 pancakes, please.

Return values

In general, Julia returns the last value to come from the last calculation within the block:


In [9]:
function dinner(sausages, mash)
           cost_of_sausages = sausages * 0.85
           cost_of_mash = (mash == true ? 0.60 : 0.00) # Ternary operator, condition ? run if true : run if false
    cost_of_sausages + cost_of_mash # No formal return statement, just last step in calculation
       end


Out[9]:
dinner (generic function with 1 method)

In [10]:
dinner(2, true) # 2 sausages and yes to mash


Out[10]:
2.3

While we haven't told Julia what we exactly want the function to return, it infers that it would probably be the result of the last calculation (cost_of_sausages + cost_of_mash).

Now imagine that the fictitious canteen, who are so keen on calculating the cost of sausages and mash for dinner, get back to you and want the function to be changed. They are, it turns out, only interested in the cost of sausages.

You could simply put cost_of_sausages to the very end of the function, before the end keyword, or you could use the return keyword, which will tell the function what to give back. Let's redefine dinner(sausages, mash) to fit the canteen's expectations using the return keyword:


In [14]:
# Method 1: No return
function dinner(sausages, mash)
           cost_of_sausages = sausages * 0.85
           cost_of_mash = (mash == true ? 0.60 : 0.00)
           cost_of_sausages
       end


Out[14]:
dinner (generic function with 1 method)

In [15]:
dinner(2, true)


Out[15]:
1.7

In [16]:
# Method 2: Explicit return
function dinner(sausages, mash)
           cost_of_sausages = sausages * 0.85
           cost_of_mash = (mash == true ? 0.60 : 0.00)
           return cost_of_sausages
       end


Out[16]:
dinner (generic function with 1 method)

In [17]:
dinner(2, true)


Out[17]:
1.7

As a matter of style, return is a good idea to use, even if the function would return the right value. Whoever ends up debugging the script will be grateful you told them what exactly a function ends up returning.

Variable numbers of positional arguments: ... ('splats')

The above simple function had a definite number of arguments that had to be in a particular order.

Arguments where the identity of the particular argument is determined by its position among the arguments are called positional arguments – so in the example above, Julia knew the argument 2 related to sausages, not mash, because that's the position in which it was defined.

But what if you don't know how many inputs you are likely to get for a particular function? Let us imagine a function, called shout(), that shouts the patrons' orders back to the short-order cook. Some customers want a long list of items, others just one or two.

One way to implement this is to expect an array argument:


In [20]:
function shout(food_array)
    food_items = join(food_array, ", ", " and ") 
    # Join food_array elements with a commma, using an optinal final delimiter "and" to joins last two strings
    println("Get this guy $food_items\!")
    # Print the new food_items string
end


Out[20]:
shout (generic function with 1 method)

Invoking this with two arguments, we get


In [21]:
shout(["some pancakes", "sausages with gravy"])


Get this guy some pancakes and sausages with gravy!

These are returned to the function as a tuple listing each element covered by the splats.

What, however, if someone has more of an appetite? Our shout() function can accommodate it - the array just needs to get larger. This approach is perfectly viable, but regarded as a bit clumsy. What if I forget the array square brackets, for instance?


In [22]:
shout("pancakes") # Treats each character in the string as an element in an array


Get this guy p, a, n, c, a, k, e and s!

Well, that's not quite what I wanted! Fortunately, Julia allows us to have not merely multiple arguments but indeed an indefinite number.

We effect this by suffixing the variable we wish to hold the positional arguments with three full stops ..., also known as a 'splat':


In [28]:
function shout_mi(foods...)
    food_items = join(foods, ", ", " and ") 
    # Join all of the arguments (up to ...) 
    println("Get this guy some $food_items\!")
end


Out[28]:
shout_mi (generic function with 1 method)

Now our function performs perfectly, whether our customer is ravenous or he just wants some pancakes:


In [29]:
shout_mi("pancakes")


Get this guy some pancakes!

In [30]:
shout_mi("pancakes", "sausages", "gravy", "a milkshake")


Get this guy some pancakes, sausages, gravy and a milkshake!

What, however, if our customer does not seem to say anything? We would expect this to raise an error... but it doesn't:


In [31]:
shout_mi() # Does not error, `...` takes 0 to N arguments


Get this guy some !

Therefore, we need be mindful of using a splatted positional argument right in the beginning, since it will accept the input of, well, no input!

A common way to fix this is to require one positional argument, then add a splatted second argument. This way, if the function is called with no arguments at all, it will raise an error.

A better way, perhaps, is to simply test for it ourselves.

The foods variable is passed on to us as a tuple, which is the default collection type functions return and accept:


In [67]:
function test(argument...)
    println("These ", length(argument)," argument(s) in this function are are a Tuple of type ", typeof(argument),".")
end

test()
test(1,2,3)
test("hello","world")
test([1,2,3,4,5])


These 0 argument(s) in this function are are a Tuple of type Tuple{}.
These 3 argument(s) in this function are are a Tuple of type Tuple{Int64,Int64,Int64}.
These 2 argument(s) in this function are are a Tuple of type Tuple{ASCIIString,ASCIIString}.
These 1 argument(s) in this function are are a Tuple of type Tuple{Array{Int64,1}}.

Therefore, a better way to handle no input, is to simply test for it ourselves:


In [68]:
function bulletproof_shout(foods...)
    if length(foods) > 0
        println("Get this guy some $(join(foods, ", ", " and "))\!")
    else
        error("The customer needs to order something!")
    end
end


Out[68]:
bulletproof_shout (generic function with 1 method)

We use length() function on this tuple (although do note, we do test explicitly for length(foods) > 0: a result of zero would not be 'falsey', so testing simply for if length(foods) would not cut it!).

This will indicate how many elements the tuple has and raise an error if it is zero:


In [63]:
bulletproof_shout("sausages", "pancakes", "gravy")


Get this guy some sausages, pancakes and gravy!

In [64]:
bulletproof_shout("sausages")


Get this guy some sausages!

In [65]:
bulletproof_shout()


LoadError: The customer needs to order something!
while loading In[65], in expression starting on line 1

 in bulletproof_shout at In[32]:5

Finally, the function works.

The last marginal case that you might want to deal with is when the customer's order consists of an empty string "" or is the wrong type. These are further marginal cases and will not be explored here (although we will be looking at user input in quite a bit of detail in the second part of the book).

The take-away is this - a good function (one you would let your grandmother use) needs to cater for a range of marginal cases and inputs.

Splats are, however, somewhat performance-consuming and are best avoided in code that needs to run fast. In such situations, usability and performance need to be weighed and balanced.

Optional positional arguments

Positional arguments may be 'optional'. This does not mean they are not used - they are optional only from the user's perspective, who will not be required to enter them.

A perhaps better way to put this is that these arguments have default values that take effect if they are not provided at invocation. Consider the following function, which accepts 2D as well as 3D coordinates, and sets 2D coordinates, by default, on the z = 0 plane:


In [72]:
function coords(x, y, z = 0) # z is defaulted to 0 at creation
    return(x,y,z)
end


Out[72]:
coords (generic function with 2 methods)

The result:


In [73]:
coords(1, 6, 7)


Out[73]:
(1,6,7)

In [74]:
coords(3, 1)


Out[74]:
(3,1,0)

Setting defaults allows you to prevent the inevitable error that would be triggered if z = 0 were not provided for.

Consider, for instance, what would happen if the value for y, for which no default value has been set, were to be missing:


In [76]:
coords(3) # Missing y value


LoadError: MethodError: `coords` has no method matching coords(::Int64)
Closest candidates are:
  coords(::Any, !Matched::Any)
  coords(::Any, !Matched::Any, !Matched::Any)
while loading In[76], in expression starting on line 1

Julia is telling us, in its somewhat odd grammar, that the function coords() is not defined for a single input. It requires at least two arguments. This is the coords(::Any, !Matched::Any) bit.

The second line refers to the fact that it can also take on an additional third argument of type Any, which in this case happens to be the z variable we defaulted.

We can set this default (optional) variable as we see fit:


In [79]:
coords(3, 4, π) # Overwrite z


Out[79]:
(3,4,π = 3.1415926535897...)

Keyword arguments

The drawback of positional arguments is that getting the order right can be an inconvenience.

Wouldn't it be much easier, not the least from a documentation perspective, if we were allowed to give arguments names and use these names at invocation?

With Julia, you can do so at your heart's content, as long as you:

  • Put them at the end of your variables when defining the function
  • Delimit keyword arguments from non-keyword arguments with a semicolon ;, as in this snippet:

In [81]:
function buzzphrase(verb, adjective; subject="defence", goal="world peace")
    println("$(verb)ing $adjective $subject for $goal.")
end


Out[81]:
buzzphrase (generic function with 1 method)

In this function, verb and adjective are necessary positional arguments:


In [88]:
buzzphrase("defend", "rad") # delivers both requires arguments


defending rad defence for world peace.

In [87]:
buzzphrase("defend") # errors because insufficient arguments


LoadError: MethodError: `buzzphrase` has no method matching buzzphrase(::ASCIIString)
Closest candidates are:
  buzzphrase(::Any, !Matched::Any)
while loading In[87], in expression starting on line 1

However, you can use the keyword argument syntax for subject and goal. As you can see, both have defined default values which is necessary for keyword arguments in Julia.

Therefore:


In [82]:
buzzphrase("leverag", "effective", subject="best practices", goal="increased margins") # subject, then goal keywords


leveraging effective best practices for increased margins.

is equivalent to:


In [83]:
buzzphrase("leverag", "effective", goal="increased margins", subject="best practices") # goal then subject keywords


leveraging effective best practices for increased margins.

The order of keyword arguments is irrelevant, anything after the ; syntax in the function argument definition is not positionl.

and yield the same results.

Stabby Lambda functions: ->

Sometimes, you're in a hurry and need a throwaway function.

Whether it's for mapping an Array or comparing values in a sort, sometimes you don't want to define a function. A number of languages refer to these as anonymous functions, because they do not have a defined name, or reserve a lambda keyword for this, harkening back to Alonzo Church's 'lambda calculus' well before the advent of modern computers.

Julia has a stylised arrow ->, leading to the name stabby lambda for such functions.

Assume you want to map the array of all primes under 10 [2,3,5,7] to a function f so that f(x) = 2x^3 + x^2 - 2x + 4.

In case you're unfamiliar with map() functions, here's the elevator pitch: map functions take a function and an iterable and return an iterable of equal length, each element of which will be the result of feeding an element of the original iterable into the function. In this way, we "map" the initial values to final values via a transformative function.

Here's a post of it being used in Python, and a little visual:

In Julia, map() takes two arguments - a function and an iterable.

For the former, you can use a function defined in advance or use the stabby lambda notation that is the subject of this section.

The mapping function would be written in the "stabby" / lambda notation as


In [89]:
x -> 2x^3 + x^2 - 2x + 4


Out[89]:
(anonymous function)

somewhat similar to the maplet notation in mathematics. Thus, we would use the map function as follows:


In [90]:
map(x -> 2x^3 + x^2 - 2x + 4, [2, 3, 5, 7]) # maps anonymous function against array


Out[90]:
4-element Array{Int64,1}:
  20
  61
 269
 725

The stabby lambda is a little controversial, being even discouraged where it serves as a mere wrapper by the official Julia Style Guide, for the reason that such functions are impossible to unit test and can make code confusing.

In general, the advice that is often given to, and by, Python programmers about lambdas in Python holds for their stabby Julia equivalents: a stabby lambda should be obviously and unambiguously true, that is, it should be evident at first glance

  • what it does,
  • how it does what it does, and
  • that it does what it's supposed to do correctly.

In other words, consider a stabby lambda a sort of 'special pleading' - you're arguing that the function is so trivially true, defining it in a long and extensive way would benefit the code less than what is gained by the brevity of the stabby lambda syntax.

do blocks

do blocks are another form of anonymous functions. Similarly to stabby lambdas, they introduce a functional process that doesn't need to be defined by name.

Let's consider the stabby lambda in the previous example and try to rewrite it as a do block:


In [91]:
map([2, 3, 5, 7]) do x
    2x^3 + x^2 - 2x + 4
end


Out[91]:
4-element Array{Int64,1}:
  20
  61
 269
 725

The do block is a bit of syntactic sugar that helps us avoid unduly long stabby lambdas, as well as do slightly more complex things that the stabby lambda's restricted format might not allow for, such as more complex testing than a stabby lambda coupled with a ternary operator chain would allow:


In [98]:
# Do block with an unambigous and clear control flow 

map([2, 3, 5, 7]) do x
    if mod(x, 3) == 0
        x^2 + 2x - 4
    elseif mod(x, 3) == 1
        2x^3 + x^2 - 2x + 4
    else
        2x-4
    end
end


Out[98]:
4-element Array{Int64,1}:
   0
  11
   6
 725

versus:


In [99]:
# Lambda with a ternary chain that is much more ambigious 

map(x -> mod(x, 3) == 0 ? x^2 + 2x - 4 : mod(x, 3) == 1 ?  2x^3 + x^2 - 2x + 4 : 2x-4, [2, 3, 5, 7])


Out[99]:
4-element Array{Int64,1}:
   0
  11
   6
 725

As a side note, a ternary operators can be chained, and of the form:

  <condition> ? <run anonymous lambda if true> : 
  <else if condition> ? <run anonymous lambda if true> :
  <another else if condition? <run anonymous lambda if true> :
  <else run anonymous lambda if all earlier conditions are false> )

Returning multiple values

A function needs to return a single object, but that object may take the shape of a collection containing multiple values.

If your function does return multiple values from within the function, they will be returned as a tupl (which is how they were inherited):


In [100]:
function squares(x, y)
           return x^2, y^2
       end


Out[100]:
squares (generic function with 1 method)

In [101]:
squares(2,5)


Out[101]:
(4,25)

In [102]:
typeof(squares(2,5))


Out[102]:
Tuple{Int64,Int64}

In [103]:
As we can see, the function returned two values of type `Int64`, in a tuple. 

For various reasons, you may prefer defining your own type to return, such as a composite type - this is up to you and Julia gives you considerable freedom in doing so.


LoadError: syntax: extra token "we" after end of expression
while loading In[103], in expression starting on line 1

Scope in function evaluation

Scope in function evaluation refers to the availability of variables within or outside a function. Much of what has been said about scope in blocks in general applies here, but function evaluation has some peculiar quirks that are worth mentioning.

Lexical scoping

Julia implements lexical scoping, that is, the scope of a function is inherited not from its caller but its definition. Consider the following:


In [146]:
function foo(x)
    println(x)
end


Out[146]:
foo (generic function with 2 methods)

In [113]:
function bar()
    x = 2
    foo() # Inherits the function foo, but with no arguments; does not pick up the x = 2 by default
end


Out[113]:
bar (generic function with 1 method)

In [114]:
bar() # This will error because x is not defined for the inside function


LoadError: UndefVarError: x not defined
while loading In[114], in expression starting on line 1

 in bar at In[113]:3

In [115]:
foo() # The original error that was raised in bar()


LoadError: UndefVarError: x not defined
while loading In[115], in expression starting on line 1

 in foo at In[107]:2

In [116]:
foo(2) # Does work if you define it in the argument, not the scope


2

This is not unexpected, since the assignment of x to 2 is 'not visible' to the function foo when it's called.

In other words, the assignment of x is outside the scope of the function. Therefore, it does not see the variable's definition and this yields an undefined variable error.

Global variables

A variable defined in the global scope is available to all functions:


In [122]:
x = 2


Out[122]:
2

In [123]:
foo() # Does not an argument, because x is already defined globally


2

In [ ]:
@time functionwithnoglobal(x)

While this is very helpful, global variables incur an immense performance penalty:


In [150]:
function yesglobal(x) # should inherit the global variable x = 2
    x ^ 2
end


Out[150]:
needglobal (generic function with 1 method)

In [143]:
function noglobal(y) # will need a locally designated variable y 
    y ^ 2
end


Out[143]:
neednoglobal (generic function with 1 method)

In [153]:
@time yesglobal(x)


  0.000963 seconds (309 allocations: 17.440 KB)
Out[153]:
4

In [154]:
@time noglobal(2)


  0.001534 seconds (280 allocations: 15.550 KB)
Out[154]:
4

Therefore, their use is generally discouraged unless absolutely necessary.

Higher order functions

In general, the idea of a higher order function serves to distinguish functions that accept a function as an argument from other functions, sometimes referred to as first-order functions.

In functional programming, higher order functions are much more important than in OOP or other paradigms, and indeed even if you return to your OOP roots, an understanding of higher order functions will help you enormously in dealing with the implementations of higher order functions in your language of choice: since most higher-order functions are so useful for munging data, most programming languages do have implementations of map(), sort() and other archetypal higher order functions.

Functions that accept functions as arguments

We have already introduced map(), a typical higher-order function, above. While higher-order functions appear to be somewhat complex, they are actually easier than they seem.

A function is an object like any other, and so can be fed into another function as an argument. You will not, generally, need to do anything special for your function to accept a function as an argument, except make sure you are calling the function provided to you.


In [155]:
"""
Greets `x` using a string.
"""
function greet(x)
    str = x()
    println("Hello, $str\!")
end


Out[155]:
greet (generic function with 1 method)

In [157]:
"""
Returns the string "world"
"""
function tell_me_where_I_live()
    return("world")
end


Out[157]:
tell_me_where_I_live (generic function with 1 method)

In [158]:
greet(tell_me_where_I_live) # Take the string returned by the inner function, and pass it to the outer function


Hello, world!

Quite importantly, when you are passing a function to another function as an argument, you are not passing a call, you're passing the function object - so don't forget to skip the parentheses ()!


In [160]:
greet(tell_me_where_I_live()) # This won't work, since the () calls the function on something


LoadError: MethodError: `call` has no method matching call(::ASCIIString)
Closest candidates are:
  BoundsError()
  BoundsError(, !Matched::Any...)
  DivideError()
  ...
while loading In[160], in expression starting on line 1

 in greet at In[155]:5

Operators and higher-order functions

Operators, such as +, are just clever aliases for functions. Thus, there is no reason why they couldn't be passed into a function:


In [161]:
"""
Calls the function x on arguments y and z.
"""
function oper(x, y, z)
    return x(y, z)
end


Out[161]:
oper (generic function with 1 method)

In [163]:
oper(+, π, e) # Adds π and e


Out[163]:
5.859874482048838

In [164]:
oper(-, π, e) # Subtracts π and e


Out[164]:
0.423310825130748

In this case, the operator + was fed into our function (which did nothing but execute the operator fed in as x on y and z).

Functions that return functions

Just as accepting functions is perfectly permissible, a function can return a function as a result. Consider a function that returns an exponential function based on your input as the exponent.


In [166]:
"""
Creates an an anonymous expontential function of base `exponent`. 
"""
function create_exponential_function(exponent)
    exp_func = function(x) # anonymous function defintion
        return x^exponent  # that returns a function 
    end
    return exp_func # return this new anonymous function
end


Out[166]:
create_exponential_function (generic function with 1 method)

In [169]:
power_of_five = create_exponential_function(5) # Generate an anonymous function and name it


Out[169]:
(anonymous function)

In [170]:
power_of_five(5) # Use the anonymous function we've generated


Out[170]:
3125

The function above can be written more concisely with the stabby lambda syntax we encountered earlier:


In [172]:
function create_exponential_function(exponent)
    y ->  y^exponent # Create an anonymous function of this form
end


Out[172]:
create_exponential_function (generic function with 1 method)

Currying

Some languages, including some functional languages, support a feature called currying, named not after the Indian spice but after logician Haskell Curry (namesake of the Haskell language).

A curried function is one that has multiple arguments. If it is provided with values for all of them, it returns a value. If it is provided with only part of them, it returns a sort of unfinished function that takes the missing values as arguments.

Currying was proposed for Julia in 2012, but voted down, not least because it would have been difficult to accommodate within multiple dispatch.

Methods and multiple dispatch

Understanding multiple dispatch

When you call a function on a number of arguments, Julia needs to decide how exactly that function makes sense for those arguments.

n this sense, functions are not so much names for individual functions but for bunches of conceptually similar functions, with Julia deciding which particular one to call.

Consider the * operator (which, like all operators, is a function):


In [174]:
π * e


Out[174]:
8.539734222673566

In [175]:
"sausages " * "mash"


Out[175]:
"sausages mash"

As the example above shows, the * function can take various types, and it has various actions defined for each - for numeric types, this involves multiplication, while for strings, * means concatenation.

The feature of Julia that allows the call of the right implementation of a function based on arguments is called multiple dispatch, and the implementations are referred to as methods.

Each function may have a number of methods defined for various data types, and it may have no methods at all defined for some. Finally, the error message we get when we use the 'wrong' type of input starts to make sense:


In [177]:
2 * "sausage" # There is no method for what to return when you do Int64 * ASCIIString


LoadError: MethodError: `*` has no method matching *(::Int64, ::ASCIIString)
Closest candidates are:
  *(::Any, ::Any, !Matched::Any, !Matched::Any...)
  *(::Real, !Matched::Complex{Bool})
  *(::Real, !Matched::Complex{T<:Real})
  ...
while loading In[177], in expression starting on line 1

What Julia is referring to in this instance is that * is not defined for one Int64 and one ASCIIString operator.

In other words, the function * has no method defined that would take these two particular kinds, after which it then recommends various options (some fairly unexpected, for instance, ::Number * ::Bool is perfectly valid – it multiplies the ::Number by 1 if the ::Bool is true and 0 if it is false).

Building methods

To construct a method, you can simply declare the function for a particular data type.

Let's consider a function that adds numbers and concatenates strings (for now, only two of each - the function can be expanded using the splat ... syntax easily).


In [185]:
"""
Adds together two values of type Number.
"""
function merge_together(a::Number, b::Number)
    a + b
end


Out[185]:
merge_together (generic function with 1 method)

This is great. It does a great job at adding up numbers:


In [179]:
merge_together(2, π)


Out[179]:
5.141592653589793

It's less adept at doing the string concatenation part we need it to do:


In [180]:
merge_together("Sausages with", " mash")


LoadError: MethodError: `merge_together` has no method matching merge_together(::ASCIIString, ::ASCIIString)
while loading In[180], in expression starting on line 1

Therefore, we will need to define a method for merge_together() that will accept ASCIIString arguments.

When Julia tells us a method is missing, it will give us the concrete data type of the argument we have entered. This is useful, but try to resist the temptation to define merge_together for ::ASCIIString.

In general, if your use case relates not to the concrete type but to the broader, abstract type (such as ours, where our use case is really all strings, not just ASCIIString), it's good practice to use the broadest abstract type that will include only the data types that you need.

In this case, it is not ASCIIString but its abstract ancestor, AbstractString (in case you forgot your handy inheritance dendrogram, you can look at the supertype of any type by using super(ASCIIString):


In [184]:
super(super(ASCIIString)) # The lever right before the `Any` type is abstract enough


Out[184]:
AbstractString

with the name of the type you're interested in). Let's define merge_together for two ::AbstractString objects:


In [186]:
"""
Adds another method for adding together two values of type AbstractString. 
"""
function merge_together(a::AbstractString, b::AbstractString)
    a * b
end


Out[186]:
merge_together (generic function with 2 methods)

That's it, folks! Julia helpfully tells us that merge_together now has two methods. Using methods(merge_together), we can list these:


In [187]:
methods(merge_together) # What types has this function been defined for


Out[187]:
2 methods for generic function merge_together:
  • merge_together(a::Number, b::Number) at In[185]:5
  • merge_together(a::AbstractString, b::AbstractString) at In[186]:5

Let's give the second one, for strings, a try:


In [188]:
merge_together("Sausages with", " mash")


Out[188]:
"Sausages with mash"

It works!

In general, when creating a function, you need to be circumspect as to what you want to use it for and what it needs to be able to deal with.

There is no need for a function to have methods for all data types. So far, we have generally not defined the data types of arguments. This is a bad practice, and when you are building functions, you should always think of yourself as building methods at the same time, and define the types you want your function to accept.

Call order and method ambiguities

Consider the following function f:


In [189]:
function f(x)
    return x
end


Out[189]:
f (generic function with 1 method)

In [190]:
function f(x::Int)
    return x^2
end


Out[190]:
f (generic function with 2 methods)

The first definition, lacking a type restriction, is deemed by Julia to accept inputs of type Any - that is, any type.

The second method, however, only takes inputs of type Int. As such, it is more specific (or, if you please, 'further downstream on the type dendrogram').

The result is that when you call f(2), the second, more specific method will be called, even if technically, the argument 2 would be acceptable for both. This is a sensible approach, since the broader the type, the more likely that the method is intended to be a 'catch-all' to mop up cases that have not been caught by any of the subtypes.

However, for functions with multiple arguments, it is possible that there is no unique method that is more unambiguous than the others. Consider the following:


In [191]:
function g(x::Int, y)
    return 2x^2 - 2y
end


Out[191]:
g (generic function with 1 method)

In [192]:
function g(x, y::Int)
    return 2x - 2y^2
end


WARNING: New definition 
    g(Any, Int64) at In[192]:2
is ambiguous with: 
    g(Int64, Any) at In[191]:2.
To fix, define 
    g(Int64, Int64)
before the new definition.
Out[192]:
g (generic function with 2 methods)

Which of these functions is 'more definite' when called as, say, g(6, 8)? The answer is 'neither', and Julia says so via an error when when declaring the second method: WARNING: New definition g(Any, Int64) at In[192]:2 is ambiguous with: g(Int64, Any) at In[191]:2. To fix, define g(Int64, Int64) before the new definition.

It also helpfully proposes a method g(x::Int64, y::Int64) that is more specific than either of the previously defined methods, and as such capable of dealing with the indefinite middle.


In [193]:
function g(x::Int, y::Int)
    return x^2 - y^2
end


Out[193]:
g (generic function with 3 methods)

Parametric methods

A parametric method, similar to parametric types, is one in which a logical relationship is asserted between types, rather than an actual type name. You may think of parameters as 'variables' for type assertions.

The parameter - by convention, but not by necessity, T for type - is enclosed in curly braces {} and interposed between the function name and its arguments:


In [198]:
function identical_types{T}(x::T, y::T)
    end


Out[198]:
identical_types (generic function with 1 method)

In [199]:
methods(identical_types)


Out[199]:
1 method for generic function identical_types:
  • identical_types{T}(x::T, y::T)

This function would accept arguments of the same type, regardless of what that type is. You can restrict the possible values T might take based on type hierarchy:


In [200]:
function identical_numbers{T<:Number}(x::T, y::T)
    end


Out[200]:
identical_numbers (generic function with 1 method)

In [202]:
methods(identical_numbers)


Out[202]:
1 method for generic function identical_numbers:
  • identical_numbers{T<:Number}(x::T<:Number, y::T<:Number)

This function allows for any inputs that are both identical and descendants of the Number supertype.

This in contrast to the case that accepts only:


In [205]:
function divergent_numbers(x::Number, y::Number)
    end


Out[205]:
divergent_numbers (generic function with 1 method)

In [204]:
methods(divergent_numbers)


Out[204]:
1 method for generic function divergent_numbers:
  • divergent_numbers(x::Number, y::Number)

which accepts inputs that are descendants of the Number supertype, regardless of whether their type matches or not.

Inspecting methods

Entering a function object into the REPL, but not calling the function object, will indicate the number of methods under the function:


In [206]:
+


Out[206]:
+ (generic function with 171 methods)

You can inspect methods available under a function by using the method() command and passing the function or operator as argument:


In [207]:
methods(+)


Out[207]:
171 methods for generic function +:

Conclusion

In this chapter, we have learned how to deal with functions in Julia. We have learnt about their input and return syntax (Tuples), defining optional (x = value) and keyword arguments (;keyword = "value"), defining anonymous 'stabby' lambda funtions with -> and do blocks, exploring function scope (global and local), the notion of higher order functions which take functions themselves as an argument (map, reduce, etc.), the underlying notion of multiple dispatch and types that handle function methods, defining parametric functions for sets of Types ({T<:Type}), and how to inspect function methods with methods(function).